<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>scroll实现虚拟列表</title>
    <style>
      #container {
        height: 400px;
        overflow: auto;
        margin-top: 300px;
        position: relative;
      }
      #phantom {
        position: absolute;
        z-index: -1;
        top: 0;
        left: 0;
        right: 0;
      }
      #wrap {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
      }
      .child {
        border: 1px solid red;
      }
    </style>
  </head>
  <body>
    <div id="container">
      <div id="phantom"></div>
      <div id="wrap"></div>
    </div>
    <script>
      // 预估高度
      let estimatedHeight = 50,
        startIndex = -1, // 开始
        endIndex = 0, // 结束
        bufferSize = 0 // 缓冲
      const list = new Array(1000).fill().map((_, i) => {
        return {
          title: '标题' + i,
          acHeight: 30 + Math.floor(Math.random() * 200),
          el: null,
          index: i,
          height: estimatedHeight,
          top: i * estimatedHeight,
          bottom: i * estimatedHeight + estimatedHeight,
          isUpdate: false
        }
      })
      const container = document.querySelector('#container')
      const wrap = document.querySelector('#wrap')
      const phantom = document.querySelector('#phantom')
      let visiableList = [],
        visiableCount = Math.ceil(container.clientHeight / estimatedHeight)
      function setWrapHeight() {
        if (visiableList.length == 0) return
        const lastItem = visiableList[visiableList.length - 1]
        phantom.style.height =
          lastItem.bottom +
          estimatedHeight * (list.length - lastItem.index - 1) +
          'px'
      }
      function updateStartEndIndex(scrollTop) {
        const item = list.find(it => it.bottom >= scrollTop)
        const index = item ? item.index : 0
        if (index != startIndex) {
          startIndex = index
          endIndex = startIndex + visiableCount
          return true
        }
      }
      function handleScroll() {
        container.addEventListener('scroll', function () {
          // 更新索引
          const isUpdate = updateStartEndIndex(container.scrollTop)
          if (!isUpdate) return
          renderDom()
        })
      }
      function renderDom() {
        let bufferStartIndex = Math.max(0, startIndex - bufferSize),
          bufferEndIndex = Math.min(endIndex + bufferSize, list.length)
        let bottom =
          bufferStartIndex > 0 ? list[bufferStartIndex - 1].bottom : 0
        visiableList = list.slice(bufferStartIndex, bufferEndIndex)
        new Promise(resolve => {
          // 先渲染
          wrap.innerHTML = visiableList
            .map(item => {
              return `<div class='child' style='height:${item.acHeight}px;'>${item.title}</div>`
            })
            .join('')
          resolve()
        }).then(() => {
          // 偏移tranfrom
          wrap.style.transform = `translate3D(0,${bottom}px,0)`
          // 再计算实际高度
          const children = wrap.childNodes
          visiableList.forEach((item, index) => {
            item.top = bottom
            item.height = children[index].offsetHeight
            item.bottom = bottom + item.height
            bottom = item.bottom
          })
          // 更新滚动高度
          setWrapHeight()
        })
      }
      updateStartEndIndex()
      renderDom()
      handleScroll()
    </script>
  </body>
</html>
